package cn.wen.forum.service.impl;

import cn.wen.forum.common.enums.ResultCode;
import cn.wen.forum.common.exception.ServiceException;
import cn.wen.forum.common.result.JSONResult;
import cn.wen.forum.common.result.RequestParams;
import cn.wen.forum.common.result.Result;
import cn.wen.forum.common.util.LevelUtils;
import cn.wen.forum.common.util.RedisUtils;
import cn.wen.forum.common.util.StringUtils;
import cn.wen.forum.config.JWTConfig;
import cn.wen.forum.constants.ForumConstant;
import cn.wen.forum.mapper.PostMapper;
import cn.wen.forum.mapper.UserMapper;
import cn.wen.forum.pojo.doc.PostDoc;
import cn.wen.forum.pojo.entity.Post;
import cn.wen.forum.pojo.entity.User;
import cn.wen.forum.service.IPostService;
import cn.wen.forum.vo.PostVO;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.elasticsearch.*;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.ScoreSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @ClassName ForumServiceImpl
 * @Author By wen
 * @Description  post 服务层
 * @Date 2023/3/7 11:16
 * @Version 1.0
 */
@Service("forum")
public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements IPostService {


    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    public RedisUtils redisUtils;

    @Autowired
    private PostMapper postMapper;

    @Autowired
    private UserMapper userMapper;


    /**
     * 搜索框联想输入补全
     * @param key
     * @return
     */
    @Override
    public List<String> getSuggestion(String key) {
        try {
            // 1.准备请求
            SearchRequest request = new SearchRequest("forum");
            // 2.请求参数
            request.source().suggest(new SuggestBuilder()
                    .addSuggestion(
                            "forumSuggest",
                            SuggestBuilders
                                    .completionSuggestion("suggestion")
                                    .size(10)
                                    .skipDuplicates(true)
                                    .prefix(key)
                    ));
            // 3.发出请求
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            // 4.解析
            Suggest suggest = response.getSuggest();
            // 4.1.根据名称获取结果
            CompletionSuggestion suggestion = suggest.getSuggestion("forumSuggest");
            // 4.2.获取options
            List<String> list = new ArrayList<>();
            for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
                // 4.3.获取补全的结果
                String str = option.getText().toString();
                // 4.4.放入集合
                list.add(str);
            }
            return list;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 搜索帖子集合
     *
     * @param params
     * @return
     */
    @Override
    public JSONResult searchPostList(RequestParams params) {
        try {
            // 1.准备Request
            SearchRequest request = new SearchRequest("forum");
            // 2.准备请求参数
            // 2.1.query
            buildBasicQuery(params, request);
            // 2.2.分页
            int page = params.getPage();
            int size = params.getSize();
            request.source().from((page - 1) * size).size(size);
            // 2.4 设置高亮
            if(StringUtils.isNotBlank(params.getKey())){
                request.source().highlighter(new HighlightBuilder().field("title").field("content").requireFieldMatch(false));
            }
            // 3.发送请求
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            // 4.解析响应
            return handleResponse(response);
        } catch (IOException e) {
            throw new RuntimeException("搜索数据失败", e);
        }
    }

    /**
     * 根据id获取帖子数据
     *
     * @return
     */
    @Override
    public JSONResult getById(String id, HttpServletRequest request, HttpServletResponse response) {
        if(StringUtils.isBlank(id)){
            throw new ServiceException(ResultCode.POST_NOT_EXIST);
        }
        PostVO post = postMapper.selectPostAndUserInfoById(id);

        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        if(StringUtils.isNotNull(post)){
            post.setTimeStr(dateFormat.format(post.getTime()));

            // 根据帖子id作为key从redis缓存读取点赞收藏相关信息，没有则存入缓存
            Map map = redisUtils.getCacheMap(String.valueOf(post.getId()));
            if(StringUtils.isNull(map)){
                map.put(ForumConstant.APPROVAL_KEY,post.getApproval());
                map.put(ForumConstant.COLLECT_KEY,post.getCollect());
            } else{
                // 从缓存读取评论数 以及 收藏数
                post.setApproval(Integer.parseInt(map.get(ForumConstant.APPROVAL_KEY) + ""));
                if(StringUtils.isNull(map.get(ForumConstant.COLLECT_KEY))){
                    map.put(ForumConstant.COLLECT_KEY,post.getCollect());
                } else{
                    post.setCollect(Integer.parseInt(map.get(ForumConstant.COLLECT_KEY) + ""));
                }
            }

            // 将该主题帖id作为value加入redis缓存的set集合,用于定时更新 每天帖子浏览量发生变化 的帖子的数据库信息
            Set<String> idSet = redisUtils.getCacheSet(ForumConstant.POST_ID);
            if(StringUtils.isNull(idSet)){
                idSet = new HashSet<String>();
            }
            idSet.add(post.getId()+"");

            // 将当前帖子id作为key，用户id作为value加入redis的缓存，用于记录当前帖子被哪些用户浏览过。
            Set<String> uIDSet = redisUtils.getCacheSet(ForumConstant.POST_ID + post.getId());
            if(StringUtils.isNull(uIDSet)){
                uIDSet = new HashSet<String>();
            }
            // 从缓存读取浏览量
            Integer pageView = Integer.parseInt(map.get(ForumConstant.PAGEVIEW_KEY) + "");
            String UID = JWTConfig.getUser(request,response).getId() +"";

            if(uIDSet.contains(UID)){
                // 如果当前用户已经浏览过该帖子，则不计算浏览量
                post.setPageView(String.valueOf(pageView));
            } else{
                //  +1 作为更新
                map.put(ForumConstant.PAGEVIEW_KEY,pageView + 1);
                post.setPageView(String.valueOf(pageView + 1));
            }

            LinkedHashMap<Object, Object> browsing_history_map = new LimitedSizeLinkedHashMap<>(100);
            // 以用户UID联合字符串前缀为key获取当前用户的浏览记录map，浏览记录保存一百条
            browsing_history_map.putAll(redisUtils.getCacheMap(ForumConstant.USER_BROWSING_HISTORY + UID));
            // 保存当前主体贴id为map的key，时间为value,如果已经存在，则删除原有记录，然后新增
            if(StringUtils.isNotEmpty(browsing_history_map.get(post.getId())+"")){
                browsing_history_map.remove(post.getId()+"");
            }
            browsing_history_map.put(post.getId()+"",dateFormat.format(new Date()));

            // 刷新缓存的uid、浏览量信息、浏览量变化的帖子id、以及浏览记录信息
            uIDSet.add(UID);

            redisUtils.setCacheSet(ForumConstant.POST_ID + post.getId(),uIDSet);
            redisUtils.setCacheMap(String.valueOf(post.getId()),map);
            redisUtils.setCacheSet(ForumConstant.POST_ID,idSet);
            redisUtils.setCacheMap(ForumConstant.USER_BROWSING_HISTORY + UID,(Map)browsing_history_map);
        } else{
            throw new ServiceException(ResultCode.POST_NOT_EXIST);
        }

        post.setAttention("1");
        post.setLevelStr(LevelUtils.levelToLevelName(post.getLevel()));

        return Result.success(post);
    }


    /**
     * 发布帖子
     *
     * @param post
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JSONResult toPublish(Post post) throws Exception {
        post.setTime(new Date());
        post.setApproval(0);
        post.setCollect(0);
        post.setIsElite("0");
        post.setIsTop("0");
        post.setReview("0");
        post.setPageView("0");
        post.setFlag("1");
        int insert = postMapper.insert(post);
        if(insert > 0){
            // 记录发帖数
            User user = userMapper.selectById(post.getUserId());
            user.setPostNum(user.getPostNum() + 1);
            userMapper.updateById(user);
            // 写入ES
            // 1.查询数据库数据
            Post postEntity = postMapper.selectById(post.getId());
            // 2.转换为PostDoc
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            postEntity.setTimeStr(dateFormat.format(post.getTime()));
            PostDoc postDoc = new PostDoc(postEntity);
            postDoc.setTime(postEntity.getTime().getTime());
            // 3.转JSON
            String json = JSON.toJSONString(postDoc);
            // 1.准备Request
            IndexRequest request = new IndexRequest("forum").id(postDoc.getId().toString());
            // 2.准备请求参数DSL，其实就是文档的JSON字符串
            request.source(json, XContentType.JSON);
            // 3.发送请求
            try {
                restHighLevelClient.index(request, RequestOptions.DEFAULT);
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 更新经验值
            boolean isAdd = LevelUtils.updateEmpiricalValue(user.getId(),ForumConstant.ADD_TYPE_PUBLISH,false);
            if(isAdd) {
                return Result.success(ResultCode.PUBLISH_SUCCESS_ADD,post.getId());
            } else{
                return Result.success(ResultCode.PUBLISH_SUCCESS,post.getId());
            }
        }
        return Result.fail();
    }


    private void buildBasicQuery(RequestParams params, SearchRequest request) {
        // 1.准备Boolean查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        // 1.1.关键字搜索，match查询，放到must中
        String key = params.getKey();
        if (StringUtils.isNotBlank(key)) {
            // 不为空，根据关键字查询
            boolQuery.must(QueryBuilders.multiMatchQuery(key,"title.pinyin_full","content"));
           // boolQuery.must(QueryBuilders.matchQuery(, key));
        } else {
            // 为空，查询所有
            boolQuery.must(QueryBuilders.matchAllQuery());
        }
        // 1.2. 只查询正常状态帖子，帖子未删除或者屏蔽
        boolQuery.must(QueryBuilders.termQuery("flag", 1));

        // 2.算分函数查询
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
                boolQuery, // 原始查询，boolQuery
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // function数组
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.termQuery("top", true), // 过滤条件
                                ScoreFunctionBuilders.weightFactorFunction(1000) // 算分函数
                        ),
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                QueryBuilders.termQuery("elite", true), // 过滤条件
                                ScoreFunctionBuilders.weightFactorFunction(500) // 算分函数
                        )
                }
        );
        // 3.1 排序字段规则
        FieldSortBuilder sortBuilder = new FieldSortBuilder("time")
                .order(SortOrder.DESC);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        // 3.2 此处尤为重要，保证查询时：先按得分排序，再按指定字段排序，这样得到的结果加权分更高的结果在返回结果集合中要靠前
        searchSourceBuilder.query(functionScoreQuery);
        searchSourceBuilder.sort(new ScoreSortBuilder().order(SortOrder.DESC));
        searchSourceBuilder.sort(sortBuilder);

        // 4.设置查询条件
        request.source(searchSourceBuilder);
    }

    private JSONResult handleResponse(SearchResponse response) {
        SearchHits searchHits = response.getHits();
        // 4.1.总条数
        long total = searchHits.getTotalHits().value;
        // 4.2.获取文档数组
        SearchHit[] hits = searchHits.getHits();
        // 4.3.遍历
        List<PostDoc> posts = new ArrayList<>(hits.length);
        for (SearchHit hit : hits) {
            // 4.4.获取source
            String json = hit.getSourceAsString();
            // 4.5.反序列化，非高亮的
            PostDoc postDoc = JSON.parseObject(json, PostDoc.class);
            // 4.6.处理高亮结果
            // 1)获取高亮map
            Map<String, HighlightField> map = hit.getHighlightFields();
            if (map != null && !map.isEmpty()) {
                // 2）根据字段名，获取高亮结果
                HighlightField highlightField = map.get("title");
                if (highlightField != null) {
                    // 3）获取高亮结果字符串数组中的第1个元素
                    String hTitle = highlightField.getFragments()[0].toString();
                    // 4）把高亮结果放到HotelDoc中
                    postDoc.setTitle(hTitle);
                }
                // 2）根据字段名，获取高亮结果
                HighlightField highlightFieldContent = map.get("content");
                if (highlightFieldContent != null) {
                    // 3）获取高亮结果字符串数组中的第1个元素
                    String hContent = highlightFieldContent.getFragments()[0].toString();
                    // 4）把高亮结果放到HotelDoc中
                    postDoc.setContent(hContent);
                }
            }
            // 4.9.从redis获取浏览量相关信息，若没有则写入
            Map cacheMap = redisUtils.getCacheMap(String.valueOf(postDoc.getId()));
            if(StringUtils.isNull(cacheMap)){
                cacheMap.put(ForumConstant.APPROVAL_KEY,postDoc.getApproval() + "");
                cacheMap.put(ForumConstant.PAGEVIEW_KEY,postDoc.getPageView());
            } else{
                // 从缓存读取评论数以及浏览量
                postDoc.setApproval(Integer.parseInt(cacheMap.get(ForumConstant.APPROVAL_KEY) + ""));
                postDoc.setPageView(StringUtils.handlerPageViewCount(cacheMap.get(ForumConstant.PAGEVIEW_KEY) + ""));
            }
            redisUtils.setCacheMap(String.valueOf(postDoc.getId()),cacheMap);
            // 5.0.放入集合
            posts.add(postDoc);
        }
        JSONObject json = new JSONObject();
        json.put("total",total);
        json.put("postList",posts);
        return Result.success(json);
    }

  
}

/**
 * 自定义实现linedhashMap，维持map指定容量，当达到阈值则会删除最早的键值对
 * @param <K>
 * @param <V>
 */
class LimitedSizeLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
    private int maxSize;

    public LimitedSizeLinkedHashMap(int maxSize) {
        super();
        this.maxSize = maxSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxSize;
    }
}

